/**
 * Note Request Router
 * @author Wangtd
 */
const express = require('express');
const router = express.Router();
const os = require('os');
const uuid = require('uuid');
const fs = require('fs');
const path = require('path');
const multipart = require('connect-multiparty');
const svgCaptcha = require('svg-captcha');
const nodemailer = require('nodemailer');
const md5 = require('md5-node');

const config = require('../config.js');
const util = require('./util.js');
const logger = require('./logger.js');
const template = require('./template.js');
const security = require('./security.js');
const git = require('./git.js');

/*======================================== auth ========================================*/

router.get('/note/auth/captcha', function(request, response) {
    var captcha = svgCaptcha.create({ noise: 2 });
    request.session.captcha = captcha.text;
    response.type('svg');
    response.status(200).send(captcha.data);
});

router.get('/note/auth/kaptcha', function(request, response) {
    var params = request.query;
    var kaptcha = util.random(6);
    request.session.kaptcha = kaptcha;
    // send email
    sendMail(params.email, '[Cloudnote] 注册验证码', template.render('register_kaptcha', {
        kaptcha: kaptcha
    }));
    return response.end();
});

router.get('/note/auth/rememberme', function(request, response) {
    security.fetchRememberMe(request);
    if (request.session.user) {
        return response.json({
            email: request.session.user.email,
            role: request.session.user.role,
            accessible: true
        });
    } else {
        return response.json({
            accessible: security.hasPermission(request)
        });
    }
});

router.post('/note/auth/login', function(request, response) {
    var params = request.body;
    logger.info(request, 'User login: email=[%s]', params.email);
    var users = JSON.parse(util.readFileSync(config.workDir + '/users.json'));
    for (var i = 0; i < users.length; i++) {
        if (users[i].email == params.email) {
            if (md5(params.password + users[i].salt) == users[i].password) {
                request.session.user = users[i];
                // remember me
                var value = '', maxAge = 0;
                if (params.rememberMe) {
                    var token = md5(users[i].email + ':' + users[i].password);
                    value = new Buffer(token).toString('base64');
                    maxAge = 7 * 24* 3600 * 1000;// 7 days
                }
                response.cookie('remember_me', value, { maxAge: maxAge, httpOnly: true });
                return response.json({
                    email: users[i].email,
                    role: users[i].role
                });
            }
        }
    }
    response.end('user');
});

router.post('/note/auth/logout', function(request, response) {
    logger.info(request, 'User logout');
    if (request.session.user) {
        request.session.user = null;
        response.cookie('remember_me', '', { maxAge: 0, httpOnly: true });
    }
    return response.json({
        accessible: security.hasPermission(request)
    });
});

router.post('/note/auth/password', function(request, response) {
    var params = request.body;
    if (!isValidCaptcha(params.captcha, request)) {
        response.end('captcha');
    } else {
        logger.info(request, 'User edit password');
        if (request.session.user) {
            tryUserLock(function() {
                var filepath = config.workDir + '/users.json',
                    users = JSON.parse(util.readFileSync(filepath));
                for (var i = 0; i < users.length; i++) {
                    if (users[i].email == request.session.user.email) {
                        if (md5(params.password + users[i].salt) == users[i].password) {
                            // update password
                            users[i].salt = md5(uuid.v1());
                            users[i].password = md5(params.newPassword + users[i].salt);
                            users[i].modified = new Date().getTime();
                            // apply for administrator
                            if (config.security.admins.indexOf(users[i].email) != -1) {
                                users[i].role = 'admin';
                            }
                            util.writeFileSync(filepath, JSON.stringify(users, null, 2), true);
                            // clear remember me
                            response.cookie('remember_me', '', { maxAge: 0, httpOnly: true });
                            return response.end();
                        } else {
                            return response.end('password');
                        }
                    }
                }
            }, function() {
                response.end('locked');
            });
        }
        response.end('timeout');
    }
});

router.post('/note/auth/register', function(request, response) {
    var params = request.body;
    if (!isValidCaptcha(params.kaptcha, request, 'kaptcha')) {
        response.end('kaptcha');
    } else if (params.email.lastIndexOf(config.security.mailDomain) == -1) {
        response.end('domain');
    } else {
        logger.info(request, 'User register: email=[%s]', params.email);
        tryUserLock(function() {
            var filepath = config.workDir + '/users.json',
                users = JSON.parse(util.readFileSync(filepath));
            for (var i = 0; i < users.length; i++) {
                if (users[i].email == params.email) {
                    return response.end('email');
                }
            }
            // create user
            var now = new Date().getTime(),
                salt = md5(uuid.v1());
            users.push({
                email: params.email,
                password: md5(params.password + salt),
                salt: salt,
                role: 'user',
                created: now,
                modified: now
            });
            util.writeFileSync(filepath, JSON.stringify(users, null, 2), true);
            response.end();
        }, function() {
            response.end('locked');
        });
    }
});

router.post('/note/auth/reset', function(request, response) {
    var params = request.body;
    if (!isValidCaptcha(params.captcha, request)) {
        response.end('captcha');
    } else {
        logger.info(request, 'User reset password: email=[%s]', params.email);
        tryUserLock(function() {
            var filepath = config.workDir + '/users.json',
                users = JSON.parse(util.readFileSync(filepath));
            for (var i = 0; i < users.length; i++) {
                if (users[i].email == params.email) {
                    // reset password
                    var password = util.random(8);
                    users[i].salt = md5(uuid.v1());
                    users[i].password = md5(password + users[i].salt);
                    users[i].modified = new Date().getTime();
                    util.writeFileSync(filepath, JSON.stringify(users, null, 2), true);
                    // send email
                    sendMail(params.email, '[Cloudnote] 密码重置', template.render('reset_password', {
                        password: password
                    }));
                    return response.end();
                }
            }
            response.end('email');
        }, function() {
            response.end('locked');
        });
    }
});

/*======================================== note ========================================*/

router.put('/note/module/save', function(request, response) {
    var filepath = path.join(config.noteDir, request.body.module);
    logger.info(request, 'Create module: %s', filepath);
    if(!fs.existsSync(filepath)) {
        fs.mkdirSync(filepath);
    }
    response.json(true);
});

router.put('/note/module/rename', function(request, response) {
    var params = request.body;
    if (requireModule(params.oldModule)) {
        return response.end('required');
    }
    var oldPath = path.join(config.noteDir, params.oldModule),
        newPath = path.join(config.noteDir, params.newModule);
    logger.info(request, 'Rename module: %s => %s', oldPath, newPath);
    if(fs.existsSync(newPath)) {
        response.json(false);
    } else {
        // git commit
        var email = request.session.user.email;
        var message = util.format('Rename module: %s => %s', path.relative(config.noteDir, oldPath),
                path.relative(config.noteDir, newPath));
        git.commitFiles(function() {
            // rename module
            fs.renameSync(oldPath, newPath);
        }, email, message);
        response.json(true);
    }
});

router.delete('/note/module/delete', function(request, response) {
    if (requireModule(request.query.module)) {
        return response.end('required');
    }
    var filepath = path.join(config.noteDir, request.query.module);
    logger.info(request, 'Delete module: %s', filepath);
    // git commit
    var email = request.session.user.email;
    var message = util.format('Delete module: %s', path.relative(config.noteDir, filepath));
    git.commitFiles(function() {
        util.deleteAllSync(filepath);
    }, email, message);
    response.json(true);
});

router.get('/note/data/tree', function(request, response) {
    if (request.query.basedir) {
        var node = { path: request.query.basedir };
        var basedir = path.join(config.noteDir, request.query.basedir);
        if (fs.existsSync(basedir)) {
            // node files
            readDirSync(basedir, node, request.query.dironly);
            response.json(node);
        } else {
            response.end('404');
        }
    } else {
        var modules = [];
        // module name
        var files = fs.readdirSync(config.noteDir);
        for (var i in files) {
            var filename = files[i];
            var file = fs.statSync(config.noteDir + '/' + filename);
            if(file.isDirectory()) {
                if (filename.indexOf('.') != 0) {// .git
                    var module = { name: filename, module: true };
                    // module files
                    readDirSync(config.noteDir + '/' + filename, module, request.query.dironly);
                    modules.push(module);
                }
            }
        }
        response.json(modules);
    }
});

router.get('/note/data/read', function(request, response) {
    var filepath = path.join(config.noteDir, request.query.file);
    if (fs.existsSync(filepath)) {
        response.json({
            data: fs.readFileSync(filepath, 'utf-8'),
            modified: fs.statSync(filepath).mtime.getTime()
        });
    } else {
        response.end('404');
    }
});

router.post('/note/data/save', function(request, response) {
    var params = request.body;
    if (requireModule(params.module)) {
        return response.end('required');
    }
    var filepath = path.join(config.noteDir, params.module, params.path);
    if (params.type == 'folder') {
        logger.info(request, 'Create directory: %s', filepath);
        if(fs.existsSync(filepath)) {
            response.json(false);
        } else {
            util.mkdirsSync(filepath);
            response.json(true);
        }
    } else if (params.type == 'file') {
        logger.info(request, 'Save file: path=%s, size=%d', filepath, params.data.length);
        if(params.created && fs.existsSync(filepath)) {
            response.json(false);
        } else {
            var modified = null;
            var action = '';
            if (fs.existsSync(filepath)) {
                modified = fs.statSync(filepath).mtime.getTime();
                if (modified != params.modified) {
                    return response.end('expired');
                }
                action = 'Update file';
            } else {
                util.mkdirsSync(path.dirname(filepath));
                action = 'Create file';
            }
            // git commit
            var email = request.session.user.email;
            var message = util.format('%s: %s', action, path.relative(config.noteDir, filepath));
            git.commitFiles(function() {
                util.writeFileSync(filepath, params.data, true);
            }, email, message);
            response.json(true);
        }
    }
});

router.post('/note/data/rename', function(request, response) {
    var params = request.body;
    if (requireModule(params.module)) {
        return response.end('required');
    }
    var oldPath = path.join(config.noteDir, params.module, params.oldPath),
        newPath = path.join(config.noteDir, params.module, params.newPath);
    logger.info(request, 'Rename path: %s => %s', oldPath, newPath);
    if(fs.existsSync(newPath)) {
        response.json(false);
    } else {
        var action = '';
        if (fs.statSync(oldPath).isDirectory()) {
            action = 'Rename directory';
        } else {
            action = 'Rename file';
        }
        // git commit
        var email = request.session.user.email;
        var message = util.format('%s: %s => %s', action, path.relative(config.noteDir, oldPath),
                path.relative(config.noteDir, newPath));
        git.commitFiles(function() {
            fs.renameSync(oldPath, newPath);
        }, email, message);
        response.json(true);
    }
});

router.delete('/note/data/delete', function(request, response) {
    var params = request.query;
    if (requireModule(params.module)) {
        return response.end('required');
    }
    var filepath = path.join(config.noteDir, params.module, params.path);
    logger.info(request, 'Delete path: %s', filepath);
    var action = 'Delete file';
    if (fs.statSync(filepath).isDirectory()) {
        action = 'Delete directory';
    }
    // git commit
    var email = request.session.user.email;
    var message = util.format('%s: %s', action, path.relative(config.noteDir, filepath));
    git.commitFiles(function() {
        util.deleteAllSync(filepath);
    }, email, message);
    response.json(true);
});

router.post('/note/data/copy', function(request, response) {
    var params = request.body;
    if (requireModule(params.module)) {
        return response.end('required');
    }
    var source = path.join(config.noteDir, params.source);
    var target = path.join(config.noteDir, params.module, params.target);
    // copy & delete
    var email = request.session.user.email;
    if (params.type == 'folder') {
        logger.info(request, 'Copy directory: %s => %s', source, target);
        params.ignored.root = true;
        // git commit
        var message = util.format('Copy directory: %s => %s', path.relative(config.noteDir, source),
                path.relative(config.noteDir, target));
        git.commitFiles(function() {
            copyFolder(source, target, params.ignored);
        }, email, message);
        // move directory
        if (params.movable) {
            logger.info(request, 'Delete directory: %s', source);
            // git commit
            message = util.format('Delete directory: %s', path.relative(config.noteDir, source));
            git.commitFiles(function() {
                util.deleteAllSync(source);
            }, email, message);
        }
    } else if (params.type == 'file') {
        logger.info(request, 'Copy file: %s => %s', source, target);
        var filepath = path.dirname(target);
        if(!fs.existsSync(filepath)) {
            util.mkdirsSync(filepath);
        }
        // git commit
        var message = util.format('Copy file: %s => %s', path.relative(config.noteDir, source),
                path.relative(config.noteDir, target));
        git.commitFiles(function() {
            copyFile(source, target, params.ignored);
        }, email, message);
        // move file
        if (params.movable) {
            logger.info(request, 'Delete file: %s', source);
            // git commit
            message = util.format('Delete file: %s', path.relative(config.noteDir, source));
            git.commitFiles(function() {
                util.deleteAllSync(source);
            }, email, message);
        }
    }
    response.json(true);
});

router.post('/note/git/logs', function(request, response) {
    var params = request.body;
    var options = {
        parent: params.parent,
        file: params.file,
        sha: params.sha
    };
    git.walkHistory(params.step, options).then(function(history) {
        response.json(history);
    }).catch(function(e) {
        response.status(500).end(e.toString());
    });
});

router.post('/note/git/log', function(request, response) {
    var params = request.body;
    var file = path.join(config.noteDir, params.file);
    git.walkHistoryFile(file, params.step, params.sha).then(function(history) {
        response.json(history);
    }).catch(function(e) {
        response.status(500).end(e.toString());
    });
});

router.post('/note/git/file', function(request, response) {
    var params = request.body;
    var file = path.join(config.noteDir, params.file);
    git.readFile(file, params.sha).done(function(data) {
        response.end(data);
    });
});

router.post('/note/git/diff', function(request, response) {
    var params = request.body;
    var file = path.join(config.noteDir, params.file);
    git.diffCommits(file, params.sha, params.shb).then(function(lines) {
        response.json(lines);
    }).catch(function(e) {
        response.status(500).end(e.toString());
    });
});

router.post('/note/git/revert', function(request, response) {
    var params = request.body;
    var file = path.join(config.noteDir, params.file);
    var email = request.session.user.email;
    var message = util.format('Revert file: %s', path.relative(config.noteDir, file));
    git.revertFile(file, params.sha, email, message).then(function() {
        response.json(true);
    }).catch(function(e) {
        response.status(500).end(e.toString());
    });
});

router.post('/note/image/upload', multipart(), function(request, response) {
    var filename = '';
    if (request.files) {
        var file = request.files['file'];
        filename = md5(uuid.v1()) + path.extname(file.name);
        var filepath = path.join(config.imageDir, filename);
        // copy file
        fs.createReadStream(file.path).pipe(fs.createWriteStream(filepath));
        // delete file
        fs.unlinkSync(file.path);
    }
    response.end(filename);
});

router.get('/note/images/:filename', function(request, response) {
    var filename = request.params.filename;
    var filepath = path.join(config.imageDir, filename);
    if (fs.existsSync(filepath)) {
        var extname = path.extname(filename).substring(1);
        response.writeHead(200, {
            'content-type': 'image/' + (extname == 'jpg' ? 'jpeg' : extname)
        });
        // response images
        response.write(fs.readFileSync(filepath, 'binary'), 'binary');
        response.end();
    } else {
        response.status(404).end();
    }
});

/*======================================== tool ========================================*/

router.post('/note/file/read', multipart(), function(request, response) {
    var content = '';
    if (request.files) {
        var file = request.files['file'];
        content = util.readFileSync(file.path);
        // delete file
        fs.unlinkSync(file.path);
    }
    response.writeHead(200, {
        'content-type': 'text/plain;charset=utf-8'
    });
    response.end(content);
});

router.post('/note/file/write', function(request, response) {
    var filename = request.body.filename,
        filepath = path.join(os.tmpdir(), uuid.v1() + path.extname(filename));
    // save file
    util.writeFileSync(filepath, request.body.content, true);
    response.writeHead(200, {
        'content-type': 'application/octet-stream',
        'content-disposition': 'attachment;filename=' + encodeURI(filename)
    });
    // response stream
    fs.createReadStream(filepath, 'utf-8').on('end', function() {
        fs.unlinkSync(filepath);
    }).pipe(response);
});

function copyFolder(source, target, ignored) {
    var copyable = ignored.root || ignored.folder;
    if (!fs.existsSync(target)) {
        copyable = true;
        util.mkdirsSync(target);
    }
    if (copyable) {
        ignored.root = false;
        var files = fs.readdirSync(source);
        for (var i in files) {
            var filename = files[i];
            var curpath = path.join(source, filename);
            var tgtpath = path.join(target, filename);
            if(fs.statSync(curpath).isDirectory()) {
                // copy folder
                copyFolder(curpath, tgtpath, ignored);
            } else {
                // copy file
                copyFile(curpath, tgtpath, ignored);
            }
        }
    }
}

function copyFile(source, target, ignored) {
    if (!fs.existsSync(target) || ignored.file) {
        util.copyFileSync(target, source, true);
    }
}

function readDirSync(filedir, srcnode, dironly) {
    var children = [];
    var files = fs.readdirSync(filedir);
    for (var i in files) {
        var filename = files[i];
        var file = fs.statSync(filedir + '/' + filename);
        if(file.isDirectory()) {
            var subnode = {};
            subnode.name = filename;
            subnode.folder = true;
            readDirSync(filedir + '/' + filename, subnode, dironly);
            children.push(subnode);
        } else if (!dironly) {
            var subnode = {};
            subnode.name = filename;
            subnode.file = true;
            children.push(subnode);
        }
    }
    srcnode.children = children;
}

function isValidCaptcha(captcha, request, sessionKey) {
    if (!sessionKey) {
        sessionKey = 'captcha';
    }
    return new RegExp('^' + captcha + '$', 'i').test(request.session[sessionKey]);
}

function requireModule(module) {
    if (!module) {
        return true;
    } else {
        var dirpath = path.join(config.noteDir, module);
        if(!fs.existsSync(dirpath)) {
            return true;
        }
    }
    return false;
}

function sendMail(account, subject, html) {
    var transport = nodemailer.createTransport(config.mailConfig);
    transport.sendMail({
        from: config.mailConfig.auth.user,
        to: account,
        subject: subject,
        html: html
    }, function(error) {
        if (error) {
            console.log(error);
        } else {
            logger.info(request, 'Send mail success: account=[%s]', account);
        }
    });
}

function tryUserLock(resolve, reject) {
    tryLock(resolve, reject, '/users.lock');
}

function tryNoteLock(resolve, reject) {
    tryLock(resolve, reject, '/notes.lock');
}

function tryLock(resolve, reject, lock) {
    var enabled = false,
        lockpath = config.workDir + lock;
    try {
        if(fs.existsSync(lockpath)) {
            reject();
        } else {
            try {
                fs.writeFileSync(lockpath, 'locked!', { flag: 'wx' });
                enabled = true;
                resolve();
            } catch (e) {
                reject();
            }
        }
    } finally {
        if (enabled) {
            fs.unlinkSync(lockpath);
        }
    }
}

module.exports = router;
